package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    @Autowired
    private RestHighLevelClient client;


    /**
     * 根据用户填写关键字 分页 获取酒店列表
     *
     * @param requestParams 参数：关键字，页大小，页码，排序字段
     * @return
     */
    @Override
    public PageResult search(RequestParams requestParams) throws IOException {
        //1.构建查询请求对象 指定查询索引库名称
        SearchRequest searchRequest = new SearchRequest("hotel");
        //2.构建请求参数对象 分组DSL查询中各种参数：query from size sort highlight
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        //2.1 设置查询方式 query 采用 全文检索 查询
        QueryBuilder basicQuery = basicQuery(requestParams);
        //sourceBuilder.query(basicQuery);


        //2.2 设置分页 from size
        Integer page = requestParams.getPage();
        int from = (page - 1) * requestParams.getSize();
        sourceBuilder.from(from).size(requestParams.getSize());

        //2.3 设置用户所在位置 进行距离排序（升序）
        if (StringUtils.isNotBlank(requestParams.getLocation())) {
            sourceBuilder.sort(
                    SortBuilders.geoDistanceSort("location", new GeoPoint(requestParams.getLocation()))
                            .unit(DistanceUnit.KILOMETERS)
                            .order(SortOrder.ASC)
            );
        }

        //2.4 在前面提交请求参数基础上，增加算分函数   1.过滤条件：文档中isAd等于true  2：算分权重 10  3.加权方式乘积
        FunctionScoreQueryBuilder isAd = QueryBuilders.functionScoreQuery(basicQuery,
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.termQuery("isAd", true),
                                ScoreFunctionBuilders.weightFactorFunction(10))
                }
        );
        sourceBuilder.query(isAd);

        //3.将请求参数加入到查询请求对象中
        searchRequest.source(sourceBuilder);

        //4.执行检索
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        //5. 处理响应结果 封装 PageResult
        //5.1 获取总命中记录数
        long total = response.getHits().getTotalHits().value;

        //5.2 解析响应来业务结果JSON 逐层解析
        List<HotelDoc> hotels = new ArrayList<>();
        for (SearchHit hit : response.getHits().getHits()) {
            String sourceAsString = hit.getSourceAsString();
            HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
            //5.2.1 从响应业务对象中获取距离
            Object[] sortValues = hit.getSortValues();
            if (sortValues != null && sortValues.length > 0) {
                hotelDoc.setDistance(sortValues[0]);
            }
            hotels.add(hotelDoc);
        }
        return new PageResult(total, hotels);
    }

    /**
     * 抽取DSL中query部分参数
     *
     * @param requestParams
     * @return
     */
    private QueryBuilder basicQuery(RequestParams requestParams) {
        //1.创建封装多条件布尔查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //1.1 封装必选满足基本条件  根据关键字全文检索查询  检索all字段
        if (StringUtils.isNotBlank(requestParams.getKey())) {
            boolQuery.must(QueryBuilders.matchQuery("all", requestParams.getKey()));
        } else {
            boolQuery.must(QueryBuilders.matchAllQuery());
        }

        //1.2 设置品牌过滤
        if (StringUtils.isNotBlank(requestParams.getBrand())) {
            boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
        }

        //1.3 设置城市过滤
        if (StringUtils.isNotBlank(requestParams.getCity())) {
            boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
        }
        //1.5 设置星级条件
        if (StringUtils.isNotBlank(requestParams.getStarName())) {
            boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
        }

        //1.4 设置价格区间过滤
        if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice()));
        }
        return boolQuery;
    }


    /**
     * 根据用户提交条件对检索到业务数据进行聚合： 对城市、星级、品牌进行聚合
     *
     * @param params 搜索条件
     * @return {"品牌":["如家","汉庭"],"城市":["北京","上海"]}
     */
    @Override
    public Map<String, List<String>> getFilters(RequestParams params) throws IOException {
        //1.构建检索请求对象 指定请求方式请求路径，路径占位符：检索索引库
        SearchRequest searchRequest = new SearchRequest("hotel");

        //2.构建请求参数对象 指定请求参数 query from size hightligth aggs sort
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //2.1 聚合不需要业务数据 将size指定为0
        sourceBuilder.size(0);
        //2.2 聚合一定要在检索条件基础上完成聚合 设置查询条件
        sourceBuilder.query(basicQuery(params));

        //2.3 设置聚合 三要素：聚合类型、聚合名称、聚合字段
        //2.3.1 设置城市聚合
        String cityAggName = "city";
        sourceBuilder.aggregation(AggregationBuilders.terms(cityAggName).field("city").size(20));
        //2.3.2 设置星级聚合
        String starAggName = "starName";
        sourceBuilder.aggregation(AggregationBuilders.terms(starAggName).field("starName").size(20));
        //2.3.3 设置品牌聚合
        String brandAggName = "brand";
        sourceBuilder.aggregation(AggregationBuilders.terms(brandAggName).field("brand").size(20));

        //3.将请求参数对象封装到检索请求对象中
        searchRequest.source(sourceBuilder);

        //4.执行检索
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        //5.处理响应数据--处理聚合结果
        return handlerAggResult(response);
    }


    /**
     * 处理聚合后过滤项结果
     *
     * @param response ES返回的检索到JSON数据
     * @return 不同聚合项  以及项目列表
     */
    private Map<String, List<String>> handlerAggResult(SearchResponse response) {
        Map<String, List<String>> mapResult = new HashMap<>();
        //1.从响应JSON中获取聚合结果对象
        Aggregations aggregations = response.getAggregations();
        //2.根据聚合名称分别获取某个聚合名称的聚合结果
        List<String> aggNameList = Arrays.asList("city", "brand", "starName");
        for (String aggName : aggNameList) {
            Terms aggregation = aggregations.get(aggName);
            List<String> strings = new ArrayList<>();
            //3.获取聚合桶内结果 获取到名称
            for (Terms.Bucket bucket : aggregation.getBuckets()) {
                String keyAsString = bucket.getKeyAsString();
                strings.add(keyAsString);
            }
            mapResult.put(aggName, strings);
        }
        return mapResult;
    }


    /**
     * 基于入参提交酒店ID  新增/修改 索引库文档
     *
     * @param id 酒店ID
     */
    @Override
    public void saveOrUpdateHotel(String id) throws IOException {
        //1.根据传入ID 查询酒店对象
        Hotel hotel = this.getById(id);
        //2.构建索引库酒店对象
        if (hotel != null) {
            HotelDoc hotelDoc = new HotelDoc(hotel);
            //3.调用ES提供API完成文档新增
            IndexRequest request = new IndexRequest("hotel").id(id);
            request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
            client.index(request, RequestOptions.DEFAULT);
        }

    }

    /**
     * 删除文档
     *
     * @param id
     */
    @Override
    public void deleteHotel(String id) throws IOException {
        DeleteRequest deleteRequest = new DeleteRequest("hotel", id);
        client.delete(deleteRequest, RequestOptions.DEFAULT);
    }

}
